/*
 * Copyright (C) 2007 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package in.xiandan.mmrc.utils;


import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

/**
 * Provides utility methods for working with byte arrays and I/O streams.
 *
 * @author Chris Nokleberg
 * @author Colin Decker
 * @since 1.0
 */
public final class ByteStreams {

    private static final int BUF_SIZE = 0x1000; // 4K

    private ByteStreams() {
    }

    /**
     * Copies all bytes from the input stream to the output stream. Does not close or flush either
     * stream.
     *
     * @param from the input stream to read from
     * @param to   the output stream to write to
     * @return the number of bytes copied
     * @throws IOException if an I/O error occurs
     */
    public static long copy(InputStream from, OutputStream to)
            throws IOException {
        Preconditions.checkNotNull(from);
        Preconditions.checkNotNull(to);
        byte[] buf = new byte[BUF_SIZE];
        long total = 0;
        while (true) {
            int r = from.read(buf);
            if (r == -1) {
                break;
            }
            to.write(buf, 0, r);
            total += r;
        }
        return total;
    }

    /**
     * Reads some bytes from an input stream and stores them into the buffer array
     * {@code b}. This method blocks until {@code len} bytes of input data have
     * been read into the array, or end of file is detected. The number of bytes
     * read is returned, possibly zero. Does not close the stream.
     *
     * <p>A caller can detect EOF if the number of bytes read is less than
     * {@code len}. All subsequent calls on the same stream will return zero.
     *
     * <p>If {@code b} is null, a {@code NullPointerException} is thrown. If
     * {@code off} is negative, or {@code len} is negative, or {@code off+len} is
     * greater than the length of the array {@code b}, then an
     * {@code IndexOutOfBoundsException} is thrown. If {@code len} is zero, then
     * no bytes are read. Otherwise, the first byte read is stored into element
     * {@code b[off]}, the next one into {@code b[off+1]}, and so on. The number
     * of bytes read is, at most, equal to {@code len}.
     *
     * @param in  the input stream to read from
     * @param b   the buffer into which the data is read
     * @param off an int specifying the offset into the data
     * @param len an int specifying the number of bytes to read
     * @return the number of bytes read
     * @throws IOException if an I/O error occurs
     */
    public static int read(InputStream in, byte[] b, int off, int len)
            throws IOException {
        Preconditions.checkNotNull(in);
        Preconditions.checkNotNull(b);
        if (len < 0) {
            throw new IndexOutOfBoundsException("len is negative");
        }
        int total = 0;
        while (total < len) {
            int result = in.read(b, off + total, len - total);
            if (result == -1) {
                break;
            }
            total += result;
        }
        return total;
    }

    /**
     * Reads all bytes from an input stream into a byte array.
     * Does not close the stream.
     *
     * @param in the input stream to read from
     * @return a byte array containing all the bytes from the stream
     * @throws IOException if an I/O error occurs
     */
    public static byte[] toByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        copy(in, out);
        return out.toByteArray();
    }

    /**
     * Reads all bytes from an input stream into a byte array. The given
     * expected size is used to create an initial byte array, but if the actual
     * number of bytes read from the stream differs, the correct result will be
     * returned anyway.
     */
    public static byte[] toByteArray(
            InputStream in,
            int expectedSize) throws IOException {
        byte[] bytes = new byte[expectedSize];
        int remaining = expectedSize;

        while (remaining > 0) {
            int off = expectedSize - remaining;
            int read = in.read(bytes, off, remaining);
            if (read == -1) {
                // end of stream before reading expectedSize bytes
                // just return the bytes read so far
                return Arrays.copyOf(bytes, off);
            }
            remaining -= read;
        }

        // bytes is now full
        int b = in.read();
        if (b == -1) {
            return bytes;
        }

        // the stream was longer, so read the rest normally
        FastByteArrayOutputStream out = new FastByteArrayOutputStream();
        out.write(b); // write the byte we read when testing for end of stream
        copy(in, out);

        byte[] result = new byte[bytes.length + out.size()];
        System.arraycopy(bytes, 0, result, 0, bytes.length);
        out.writeTo(result, bytes.length);
        return result;
    }


    /**
     * BAOS that provides limited access to its internal byte array.
     */
    private static final class FastByteArrayOutputStream
            extends ByteArrayOutputStream {
        /**
         * Writes the contents of the internal buffer to the given array starting
         * at the given offset. Assumes the array has space to hold count bytes.
         */
        void writeTo(byte[] b, int off) {
            System.arraycopy(buf, 0, b, off, count);
        }
    }


    /**
     * Attempts to read {@code len} bytes from the stream into the given array
     * starting at {@code off}, with the same behavior as
     * {@link DataInput#readFully(byte[], int, int)}. Does not close the
     * stream.
     *
     * @param in  the input stream to read from.
     * @param b   the buffer into which the data is read.
     * @param off an int specifying the offset into the data.
     * @param len an int specifying the number of bytes to read.
     * @throws EOFException if this stream reaches the end before reading all
     *                      the bytes.
     * @throws IOException  if an I/O error occurs.
     */
    public static void readFully(
            InputStream in, byte[] b, int off, int len) throws IOException {
        int read = read(in, b, off, len);
        if (read != len) {
            throw new EOFException("reached end of stream after reading "
                    + read + " bytes; " + len + " bytes expected");
        }
    }

}
